package com;

import com.dao.UserMapper;
import org.apache.ibatis.annotations.Select;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * @author chenyuan
 * @date 2020/11/24 16:13
 */
// 手动实现简单的 Mybatis 注解开发
public class Annotation {
	public static void main(String[] args) {
		// 通过动态代理，建立一个 UserMapper 的对象
		UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(Annotation.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				// 简单的获取一下几个 Mapper 接口的相关信息
				System.out.println("方法名为 ：" + method.getName());
				System.out.println("传入的参数值为 ：" + Arrays.toString(args));
				System.out.println("返回值为 ：" + method.getReturnType());
				System.out.println("返回值中的泛型为 ：" + method.getGenericReturnType());

				// 通过 Method 获取注解信息,这里写的查询方法，就使用 Select
				Select annotation = method.getAnnotation(Select.class);
				// 进行判断，防止空指针异常
				if (annotation == null) {
					throw new RuntimeException("没有获取到相应注解");
				}
				//  得到 参数名称-参数值 的映射
				Map<String, Object> nameArgsMap = buildMethodNameArgsMap(method, args);
				// 获取注解中的 SQL 语句，这里获取出了是一个数组，
				// 因为是一个简单案例，就不像框架那么严谨了，简答处理数组的第一个值
				String[] value = annotation.value();
				// 开始解析得到的 SQL 语句，替换其中的 #{} 占位符
				// 传入 SQL 语句，参数名称-参数值 的映射（用于替换值）
				String s = parsePlaceholderArges(value[0], nameArgsMap);
				// 最后打印解析完成的 SQL
				System.out.println("解析后的 SQL ：" + s);
				return null;
			}
		});
		userMapper.getUserById(1, "呵呵");
	}

	/**
	 * 用于获得 参数名称-参数值 的映射关系
	 *
	 * @param method 方法
	 * @param args   参数信息
	 * @return
	 */
	public static Map<String, Object> buildMethodNameArgsMap(Method method, Object[] args) {
		// 建立一个 Map 用于存放映射关系
		Map<String, Object> nameArgsMap = new HashMap<>();
		// 获取所有的参数名称
		Parameter[] parameters = method.getParameters();
		// 因为在 lambda 表达式中使用的变量应为 final ，所以 index 不能直接进行 ++ 操作，
		// 所以这里进行一个简单的变化
		int[] index = {0};
		// 利用 lambda 表达式，遍历获取到的所有参数，加入 Map 中
		// 因为通过 method 获取到的方法，和 args 中参数数量、顺序都是一致的，所以可以这样操作
		Arrays.asList(parameters).forEach(parameter -> {
			// 获取参数的名字
			String name = parameter.getName();
			nameArgsMap.put(name, args[index[0]++]);
		});
		return nameArgsMap;
	}

	/**
	 * 解析、替换 占位符
	 * 利用 StringBuilder 遍历传入的 SQL ，当
	 * 遇到 #{}，时就获取其包裹的参数名，
	 * 然后从 Map 中取出对应的参数值，替换上
	 *
	 * @param sql         SQL 语句
	 * @param nameArgsMap Name—Args 的 Map
	 * @return
	 */
	public static String parsePlaceholderArges(String sql, Map<String, Object> nameArgsMap) {
		StringBuilder parseSQL = new StringBuilder();
		int length = sql.length(), count = 1;
		// 循环遍历整个 SQL
		for (int i = 0; i < length; i++) {
			char c = sql.charAt(i);
			// 如果得到了 # ，代表可能要出现占位符了，进行进一步判断
			if (c == '#') {
				// 下标后移一位，判断是不是 {，组成 #{
				int nextIndex = i + 1;
				char nextChar = sql.charAt(nextIndex);
				// 如果不是 #{ 抛异常
				if (nextChar != '{') {
					throw new RuntimeException("这里应该为 #{ \n SQL：" + parseSQL.toString()
							+ "\n index :" + nextIndex);
				}
				// 如果是，获取其中包裹的参数名称
				StringBuilder tmpSB = new StringBuilder();
				// 获取 #{} 其中的参数名
				i = parseArges(tmpSB, sql, nextIndex);
				// 经过解析后的得到的参数名称
				String argName = tmpSB.toString();
				System.out.println("解析出的第" + (count++) + "个参数名为 :" + argName);
				// 从映射的 名称-值 的 Map 中取出值
				Object argValue = nameArgsMap.get(argName);
				if (argValue == null) {
					throw new RuntimeException("找不到名为 " + argName + " 的参数");
				}
				// 直接追加在后面，就可以把 XX=#{AA} 替换为 XX=value
				parseSQL.append(argValue.toString());
				continue;
			}
			// 如果没有遇到 # ，代表还没出现占位符，也就代表这个字符是真正的 SQL 的一部分，追加上去
			parseSQL.append(c);
		}
		return parseSQL.toString();
	}

	/**
	 * 把 #{} 中包裹的参数值取出来，解析参数
	 * 前面那个方法已经得到了 { 字符对应的下标了
	 * 这个方法，就要获取 { 和 } 之间的字符串了
	 *
	 * @param tmpSB     用于存放 参数名称
	 * @param sql       SQL
	 * @param nextIndex { 字符对应的下标
	 * @return
	 */
	private static int parseArges(StringBuilder tmpSB, String sql, int nextIndex) {
		// 从 { 字符后面一位开始遍历
		for (int i = nextIndex + 1; i < sql.length(); i++) {
			char c = sql.charAt(i);
			// 如果不是 } ，表示还未到，继续匹配
			if (c != '}') {
				// 代表这个字符是 参数名称 的一部分，追加到参数名的字符串后面
				tmpSB.append(c);
				continue;
			}
			// 如果是 } 退出
			return i;
		}
		// 找到最后都没有找到 } 抛出异常
		throw new RuntimeException("这里缺少 { \n index :" + nextIndex);
	}
}
